-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
stdlib: make dot product of Hermitian matrices real #52318
Conversation
LGTM. It should be more efficient too, since the compiler should be able to optimize away the computation of the imaginary parts that are discarded. (At first I thought there might be a problem with Hermitian matrices whose elements are themselves matrices, but it's okay because |
I hadn't thought of that. It goes from 2(d^2+d) multiplications of real numbers to 2d^2, so there should be a noticeable performance increase if someone is taking billions of inner products of tiny matrices. I did a quick benchmark here, and indeed the execution time decreased by roughly the expected amount. |
I think this only holds when multiplication of the elements is commutative and since julia> Ac, Bc = [complex.(randn(3,3), randn(3, 3)) |> t -> t + t' for i in 1:2];
julia> Aq, Bq = [Quaternion.(randn(3,3), randn(3, 3), randn(3, 3), randn(3,3)) |> t -> t + t' for i in 1:2];
julia> all(ishermitian, (Ac, Bc, Aq, Bq))
true
julia> dot(Ac, Bc)
-5.958241125745531 - 1.0340753672895848e-16im
julia> dot(Aq, Bq)
QuaternionF64(12.353238961023004, 9.185676749747854, -0.2811577397685232, -25.49656747643778) |
I think what is needed for the dot product to be real is conj(a*b) = conj(a)*conj(b), which does not hold for quaternions. I think it would be sensible to restrict this method to I don't really know how to restrict it to for (T, trans, real) in [(:Symmetric, :transpose, :identity), (:(Hermitian{Complex{S}} where S <: Real), :adjoint, :real)] But surely there must be a more elegant way. |
Maybe (Note that we also want to allow |
Note that restricting it to complex/real Hermitian matrices will be a bug fix, since the current version also fails when a quaternionic matrix is wrapped in julia> dot(Aq, Bq)
QuaternionF64(-9.544231167880467, -3.8948768522921418, 26.649977520155836, 5.9007891169097455)
julia> dot(Hermitian(Aq), Hermitian(Bq))
QuaternionF64(-9.544231167880467, 0.0, 0.0, 0.0)
julia> Aq == Hermitian(Aq), Bq == Hermitian(Bq)
(true, true) Would be good to add a test for this. Should there be a separate bugfix PR that just corrects this, without changing the return type, so that it can be backported? |
Thanks, that works. Indeed, we want real Hermitian matrices as well, as I often use that myself for generality. I just didn't know |
Update: I pushed a bugfix PR to #52333 so that it can be backported. We should probably merge that one first, since it will conflict with this one. |
for (T, trans, real) in [(:Symmetric, :transpose, :identity), (:Hermitian, :adjoint, :real)] | ||
for (T, trans, real) in [ | ||
(:Symmetric, :transpose, :identity), | ||
(:(Hermitian{<:Union{Real,Complex}}), :adjoint, :real), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's slightly unfortunate that this specialization prevents this from being applicable to e.g. Hermitian{<:AbstractMatrix{<:Union{Real,Complex}}}
. Although I'm not sure how we might engineer a definition that's robust through multiple levels of nested AbstractMatrix
so it would probably be hard to do something about this right now.
I'll also remark that this specialization should also be suitable for dot(::Symmetric{<:Real},::Hermitian{<:Complex}}
, although currently it appears we miss this case. Would LinearAlgebra.RealHermSymComplexHerm
be suitable here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a design flaw of LinearAlgebra. Symmetric
should have been defined as real symmetric, and should be made a subtype of Hermitian
, as mathematically a real symmetric matrix is just a particular case of a complex Hermitian matrix, and we'd get for free all of these particular cases without having to write a method for each specifically. Then one could also have a GenericSymmetric
to deal with the more exotic cases if anyone needed them.
FWIW I did think about mixing real symmetric and complex Hermitian, but I decided against adding additional methods that were going to be rarely used. Of course, I know my opinion counts for very little here.
No, wait! I've just realized that this breaks my own use case! If we restrict it to real or complex Hermitians then it won't work with JuMP variables anymore, and when I use dot I'll fall back to the generic dot, which is not only two times slower but also will not give me a real type as answer as I wanted. Using |
On a second thought, maybe the best solution is to not restrict it to subtypes of That would fix my problem, @mikmoore 's problem, and potentially other cases as well of things that are not subtypes of Complex and Real but nevertheless behave well? I also wanted to contributed a specialized kronecker product between Hermitian matrices, but this will suffer from the same issue: the kronecker product between quaternionic Hermitian matrices is not Hermitian (again because |
Silently giving wrong answers doesn't seem acceptable for the standard library. I agree that
JuMP could add a To make it easier to extend in this way, we could implement the base method as: dot(A::Symmetric, B::Symmetric) = _dot_hermsym(A, B, transpose, identity)
dot(A::RealHermSymComplexHerm, B::RealHermSymComplexHerm) = _dot_hermsym(A, B, adjoint, real)
dot(A::Symmetric{<:Real}, B::Symmetric{<:Real}) = _dot_hermsym(A, B, transpose, identity) # fix method ambiguity
function _dot_hermsym(A, B, trans, real)
#... optimized method
end and then JuMP (or anyone else who needs the optimized method for a new type) would just need to add a 1-line method that calls (This also seems a lot cleaner than the macro-based approach.) |
It wouldn't be a one-linear because JuMP (well MutableArithmetics to be more precise) also reimplements @odow would you be fine with this? |
I see two options: (i) merge as is since this is as far as we can go for a generic stdlib, or (ii) introduce a function barrier and have an type-unrestricted "dot kernel" that can be hooked into by packages, as suggested by @stevengj. If (ii) doesn't help much in the concrete JuMP context, then I suggest to merge as is and leave an efficient mutable definition to packages. |
I would prefer (ii); it does solve the problem for JuMP, even if its devs haven't expressed interest, and will also make it possible for other packages to use the faster version as well. Think of it as future-proofing. |
@blegat is the one who knows this stuff. My only comment: isn't changing this return type from Complex to Real breaking? |
Why does it solve the problem for JuMP ? We won't be calling |
Why not? You were calling |
end | ||
|
||
dotprod = real(zero(dot(first(A), first(B)))) | ||
@inbounds if A.uplo == 'U' && B.uplo == 'U' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we stop using this? This function assumes but does not enforce 1-based indexing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The parents of Symmetric
and Hermitian
are required to be one-based at construction (see the very top of this file), so this is safe.
I know there are places where we currently call or overload |
Can we put this PR out of its misery? I'm fine with either of your options @dkarrasch , just make the call. |
If this doesn't solve the issue for |
Fine by me. Can you cherry pick the commits or do I need to revert it myself? |
Let's wait for some feedback, but if people agree, then you could push a simple revert commit. |
In the absence of further comments, let's revert the last commit and then merge (if tests pass). If we find a use case that would benefit from having an unrestricted "kernel", we could still change it to that, but it seems like the only external use case we have would not use this kernel anyway. |
This reverts commit 5097097.
Done. Failing test is unrelated. |
I'm sorry to ping you again, but is there any obstruction to merging? I really want to get this over with. |
Is there anything wrong? It's been a month. |
Sorry, no, there's nothing wrong. I'll rerun CI one more time (many tests failed, but seemingly unrelated), and then merge once tests pass. |
CI: All test passed, ASAN timeout is unrelated. |
Although mathematically the dot product between Hermitian matrices is always real, LinearAlgebra stores it in a complex variable with zero imaginary part. This prevents checking whether the value is real from the type only, which causes problems in certain applications (in my case, optimization with JuMP).
Of course, there is a simple workaround of calling it as real(dot(a,b)), but it's more elegant to produce a real type in the first place.